/*
 * Created by Jaren at 2022/7/15 20:35
 */
import 'dart:async';
import 'dart:core';
import 'dart:io';
import 'dart:typed_data';

import 'package:crclib/catalog.dart';
import 'package:inmotion_protocol/base/file/file_manager.dart';
import 'package:inmotion_protocol/base/protocol.dart';
import 'package:inmotion_protocol/common/error.dart';
import 'package:inmotion_protocol/common/response.dart';
import 'package:inmotion_protocol/lorin/constant.dart';
import 'package:inmotion_protocol/lorin/file/response.dart';
import 'package:inmotion_protocol/lorin/lorin_message.dart';
import 'package:inmotion_protocol/protocol_worker.dart';
import 'package:inmotion_protocol/util/byte_util.dart';
import 'package:inmotion_protocol/util/log.dart';

abstract class LorinFileManager extends IFileManager {
  LorinFileManagerConfig _config;
  final Context context = Context();

  ActionHook? _actionHook;

  LorinFileManager(Protocol<LorinMessage> protocol, this._config)
      : super(protocol) {
    init(_config);
  }

  @override
  void init(FileConfig config) {
    reset();
    if (config is! LorinFileManagerConfig) {
      throw Error.invalidParam;
    }
    _config = config;
  }

  @override
  Stream<FileEvent> start() {
    context.startTransferTimestamp = now();

    StreamController<FileEvent> streamController = StreamController();

    onStarted();
    openFile().then((openFileRsp) {
      if (checkInterrupted()) throw CanceledError();
      if (!openFileRsp.isSuccess()) {
        throw FileOperationStepError(
            IFileManager.stepOpenFile, openFileRsp.originError);
      }

      context.target = _config.target;
      context.retryTimes = 0;
      context.checkRetryTimes = 0;

      streamController.add(FileOpenEvent(FileEvent.fileOpened));

      context.fileHandle = openFileRsp.fileHandle;
      context.fileLength = openFileRsp.fileSize;

      if (_actionHook != null) {
        return _actionHook!.run(FileEvent.byFileOpened(), null).then((none) {
          return prepareTransfer(openFileRsp.fileSize, openFileRsp.check,
              _config.localFile, streamController);
        }).catchError((error) => throw error);
      }
      return prepareTransfer(openFileRsp.fileSize, openFileRsp.check,
          _config.localFile, streamController);
    }).catchError((error) {
      streamController.addError(error);
      return Future.value(false);
    }).then((shouldContinue) {
      if (shouldContinue) {
        // 校验
        streamController.add(FileEvent(FileEvent.prepareFileChecking));

        if (_actionHook != null) {
          return _actionHook!
              .run(FileEvent(FileEvent.prepareFileChecking), null)
              .then((none) {
            return checkFile(
                _config.target,
                context.fileHandle!,
                context.fileLength,
                context.fileLength,
                calCrc32(localFileBytes()));
          }).catchError((error) => throw error);
        }
        return checkFile(_config.target, context.fileHandle!,
            context.fileLength, context.fileLength, calCrc32(localFileBytes()));
      }
      return Future.value(false);
    }).catchError((error) {
      streamController.addError(error);
      return Future.value(false);
    }).then((shouldContinue) {
      if (shouldContinue) {
        // 校验完成
        streamController.add(FileEvent.byFileChecked());

        if (_actionHook != null) {
          return _actionHook!.run(FileEvent.byFileClosed(), null).then((value) {
            return closeFile(context.target, context.fileHandle!).then(
                (value) => {streamController.add(FileEvent.byFileClosed())});
          });
        }
        return closeFile(context.target, context.fileHandle!)
            .then((value) => {streamController.add(FileEvent.byFileClosed())});
      }
      return Future.value(false);
    }).then((ignore) {
      // ignore result of close file
      return Future.value(true);
    }).catchError((error) {
      streamController.addError(error);
      return Future.value(false);
    });
    return streamController.stream;
  }

  // 传输文件到屏幕
  Stream<FileEvent> transferFileToScreen(int fileSize) {
    context.startTransferTimestamp = now();

    StreamController<FileEvent> streamController = StreamController();

    onStarted();
    context.target = _config.target;
    context.fileHandle = 0;
    context.fileLength = fileSize;

    transferFile(context, _config.localFile, 0, fileSize, streamController)
        .then((value) {
      print("✅✅✅✅ = 开始屏幕升级 - 发送升级指令");
      // 发送下载指令
      List<int> list = [
        0xEE,
        0xB6,
        0x00,
        0x03,
        0x88,
        0x33,
        0x01,
        0xFF,
        0xFC,
        0xFF,
        0xFF
      ];

      Future<bool> send() {
        return protocol
            .startTransfer(_config.target,
                para: Uint8ClampedList.fromList(list))
            .then((value) {
          return Future(() => value.isSuccess());
        });
      }

      void complete(bool val) {
        if (val) {
          protocol
              .startUpdate(_config.target,
                  para: Uint8ClampedList.fromList([0x00]))
              .then((value) {
            if (value.isSuccess()) {
              streamController.add(FileEvent.byFileChecked());
              Future.delayed(const Duration(seconds: 20)).then((value) {
                streamController.add(FileEvent.byFileClosed());
              });
            } else {
              streamController.addError(FileOperationStepError(0x13, 0x13));
            }
          });
        } else {
          streamController.addError(FileOperationStepError(0x13, 0x14));
        }
      }

      // 开始升级
      return send().then((value) {
        if (value) {
          complete(value);
        } else {
          send().then((r) {
            complete(r);
          });
        }
      });
    });
    return streamController.stream;
  }

  @override
  void reset() {
    super.reset();
    context.retryTimes = 0;
  }

  set actionHook(ActionHook? value) {
    _actionHook = value;
  }

  Future<OpenFileResponse> openFile() {
    if (checkInterrupted()) throw CanceledError();

    return protocol
        .openFile(
      target: _config.target,
      openType: _config.openType,
      name: _config.remoteFilename,
    )
        .then((rsp) {
      if (checkInterrupted()) throw CanceledError();

      if (rsp.isSuccess()) {
        context.retryTimes = 0;
        return Future.value((rsp as OpenFileResponse));
      }
      context.fileHandle = rsp.fileHandle;
      throw FileOperationStepError(IFileManager.stepOpenFile, rsp.originError);
    }).catchError((error) {
      if (error is MsgTimeoutException) {
        if (context.retryTimes < maxRetryTimes()) {
          context.retryTimes++;
          return openFile();
        }
        throw FileOperationStepError(
            IFileManager.stepOpenFile, IFileManager.errorTimeout);
      }
      if (error is FileOperationStepError) {
        if (error.error == FileError.conflict.index) {
          // 操作冲突
          if (context.retryTimes < maxRetryTimes()) {
            context.retryTimes++;
            return closeFile(_config.target, context.fileHandle ?? 0)
                .then((ignore) {
              return openFile();
            });
          }
        }
        throw error;
      }
      throw FileOperationStepError(
          IFileManager.stepOpenFile, IFileManager.errorUnknown);
    });
  }

  Future<bool> prepareTransfer(
      int remoteFileLength,
      Uint8ClampedList remoteCheck,
      File localFile,
      StreamController progressTicker);

  Future<bool> transferFile(Context context, File file, int transferOffset,
      int remoteFileLength, StreamController progressTicker);

  Uint8List localFileBytes() {
    return _config.localFile.readAsBytesSync();
  }

  Future<bool> checkCrc32(
      Uint8ClampedList remoteCrc32, File localFile, int checkLength) {
    if (!localFile.existsSync() || localFile.lengthSync() == 0) {
      return Future.value(false);
    }
    return localFile.readAsBytes().then((fileBytes) {
      if (fileBytes.length < checkLength) {
        return false;
      }
      Uint8ClampedList localCrc32 = calCrc32(fileBytes, length: checkLength);
      return ByteUtil.arrayEquals(remoteCrc32, localCrc32);
    });
  }

  Uint8ClampedList calCrc32(Uint8List fileBytes,
      {int offset = 0, int length = 0}) {
    if (length == 0) length = fileBytes.length - offset;
    if (offset != 0 || length != fileBytes.length - offset) {
      fileBytes = fileBytes.sublist(offset, offset + length);
    }
    LogUtils.d(
        "calCrc32, bytes length: ${fileBytes.length}, offset: $offset, length: $length");
    Uint8ClampedList l = ByteUtil.num2BytesLe(
        Crc32Xz().convert(fileBytes).toBigInt().toInt(), 4);
    return l;
  }

  Uint8ClampedList subNextPkgBytes(
      List<int> fileBytes, int offset, int total, int sizePeerPkg) {
    int pkgSize = nextPkgSize(offset, total, sizePeerPkg);
    return Uint8ClampedList.fromList(
        fileBytes.sublist(offset, offset + pkgSize));
  }

  int nextPkgSize(int offset, int total, int sizePeerPkg) {
    int nextPkgSize = total - offset;
    if (nextPkgSize >= sizePeerPkg) nextPkgSize = sizePeerPkg;
    return nextPkgSize;
  }

  Future<bool> checkFile(int target, int fileHandle, int offset, int length,
      Uint8ClampedList expectCheck);

  Future<bool> closeFile(int target, int fileHandle) {
    return protocol
        .closeFile(fileHandle: fileHandle, target: target)
        .then((rsp) {
      if (rsp.isSuccess()) {
        context.retryTimes = 0;
        return Future.value(true);
      }
      throw FileOperationStepError(
          IFileManager.stepSetFileOffset, rsp.originError);
    }).catchError((error) {
      if (error is MsgTimeoutException) {
        /// ignore
        return Future.value(true);
      }
      throw error;
    });
  }

  @override
  int sizePeerPkg() {
    return protocol.maxSizePeerPkg;
  }
}

class Context {
  int target = 0;
  int? fileHandle;
  Uint8List? fileBytes;
  int fileLength = 0;

  int retryTimes = 0;
  int checkRetryTimes = 0;

  int startTransferTimestamp = 0;

  Context();
}

class LorinFileManagerConfig extends FileConfig {
  int target;
  int openType;
  String remoteFilename;

  LorinFileManagerConfig(
      this.target, this.openType, this.remoteFilename, File localFile) {
    this.localFile = localFile;
  }
}
